.. _Контрактное программирование:

Контрактное программирование
============================

.. index::
    контрактное программирование
    предусловие
    постусловие
    инвариант


Контрактное программирование - это подход к разработке программного обеспечения, при котором части программного обеспечения рассматриваются
как отдельные сущности, которые предоставляют услуги друг другу. Этот подход понимает.
что программное обеспечение может работать в соответствии со своей спецификацией до тех пор, пока поставщик и
потребитель услуг подчиняются контракту.
Возможности контрактного программирования в D предполагают использование функций в качестве единиц программных
сервисов. Как и в модульном тестировании, контрактное программирование также основано на утверждениях
проверках.
Контрактное программирование в D реализуется тремя типами блоков кода:
* блоки предусловий функций
* Блоки постусловий функций
* Блоки инвариантов структур и классов.
Мы рассмотрим блоки инвариантов и наследование контрактов в следующей главе (:ref:`Контрактное программирование для структур и классов<Контрактное программирование для структур и классов>`)
после рассмотрения структур и классов.

.. index::
    in
    out
    do

блок для предусловий (in)
-------------------------------------

Корректное выполнение функций обычно зависит от того, являются ли значения их
параметров. Например, функция квадратного корня может потребовать, чтобы ее
не может быть отрицательным. Функция, работающая с датами, может потребовать, чтобы
число месяца должно быть между 1 и 12. Такие требования к функции
называются предварительными условиями.
Мы уже встречались с подобными проверками условий в главах assert и enforce
(:ref:`глава assert и enforce<глава assert и enforce>` ). Условия на значения параметров могут быть усилены с помощью проверок assert
в определениях функций:

.. code-block:: d

    string timeToString(int hour, int minute)
    {
        assert((hour >= 0) && (hour <= 23));
        assert((minute >= 0) && (minute <= 59));
        return format("%02s:%02s", hour, minute);
    }

В контрактном программировании те же самые проверки пишутся внутри блоков
функции. Когда используется блок in или out, фактическое тело функции должно быть
должно быть указано в виде блока do:

.. code-block:: d

    import std.stdio;
    import std.string;

    string timeToString(int hour, int minute)
    in
    {
        assert((hour >= 0) && (hour <= 23));
        assert((minute >= 0) && (minute <= 59));
    }
    do
    {
        return format("%02s:%02s", hour, minute);
    }

    void main()
    {
        writeln(timeToString(12, 34));
    }


Примечание: В ранних версиях D для этой цели использовалось ключевое слово body, а не do.
Преимущество блока in в том, что все предварительные условия можно хранить вместе и
отдельно от тела функции. Таким образом, тело функции будет свободна от проверок предварительных условий.
При необходимости можно
и целесообразно иметь другие проверки утверждений внутри тела функции в качестве несвязанных
которые защищают от возможных ошибок программирования в теле функции.
Код, находящийся внутри блока in, выполняется автоматически каждый раз, когда
функция вызывается. Фактическое выполнение функции начинается только в том случае, если все
проверки assert внутри блока in пройдут. Это предотвращает выполнение функции с
недопустимыми предварительными условиями и, как следствие, избежать получения некорректных результатов.
Проверка утверждения, не пройденная внутри блока in, указывает на то, что контракт был
был нарушен вызывающей стороной.

блоки для постусловий
---------------------

Другая сторона контракта предполагает гарантии, которые предоставляет функция.
Такие гарантии называются постусловиями функции. Примером функции
с постусловием может быть функция, которая возвращает количество дней в феврале: Функция может гарантировать, что возвращаемое значение всегда будет
либо 28, либо 29.
Постусловия проверяются внутри блоков out функций.
Поскольку значение, возвращаемое функцией с помощью оператора return, не обязательно должно быть
определено как переменная внутри функции, обычно нет имени для ссылки на
возвращаемое значение. Это можно рассматривать как проблему, поскольку проверки assert внутри блока
out не могут ссылаться на возвращаемую переменную по имени.
D решает эту проблему, предоставляя способ присвоения возвращаемому значению имени сразу после
ключевого слова out. Это имя представляет собой то самое значение, которое функция возвращает в
процессе:

.. code-block:: d

    int daysInFebruary(int year)
    out (result)
    {
        assert((result == 28) || (result == 29));
    }
    do
    {
        return isLeapYear(year) ? 29 : 28;
    }


Хотя result является приемлемым именем для возвращаемого значения,
также могут использоваться другие допустимые имена.
Некоторые функции не имеют возвращаемых значений или возвращаемое значение не нужно
проверять. В этом случае блок out не указывает имя:

.. code-block:: d

    out
    {
    // ...
    }

Аналогично блокам in, блоки out выполняются автоматически после выполнения тела
функции.
Сбой проверки assert внутри блока out указывает на то, что
функция нарушила контракт.
Как было очевидно, блоки in и out являются необязательными. Учитывая
также блоки unittest, которые также являются необязательными, общие функции могут состоять максимум
из четырех блоков кода:

* in: Необязательно
* out: Необязательно
* do: Обязательно, но ключевое слово do может быть пропущено, если не
  определен блок ввода или вывода.
* unittest: Необязательный и технически не является частью определения функции, но
  обычно определяется сразу после функции.
  Вот пример, в котором используются все эти блоки:

  .. code-block:: d

    import std.stdio;

    /* Distributes the sum between two variables.
            *
            * Distributes to the first variable first, but never gives
            * more than 7 to it. The rest of the sum is distributed to
            * the second variable. */
    void distribute(int sum, out int first, out int second)
    in
    {
        assert(sum >= 0, "sum cannot be negative");
    }
    out
    {
        assert(sum == (first + second));
    }
    do
    {
        first = (sum >= 7) ? 7 : sum;
        second = sum - first;
    }

    unittest
    {
        int first;
        int second;
        // Both must be 0 if the sum is 0
        distribute(0, first, second);
        assert(first == 0);
        assert(second == 0);
        // If the sum is less than 7, then all of it must be given
        // to first
        distribute(3, first, second);
        assert(first == 3);
        assert(second == 0);
        // Testing a boundary condition
        distribute(7, first, second);
        assert(first == 7);
        assert(second == 0);
        // If the sum is more than 7, then the first must get 7
        // and the rest must be given to second
        distribute(8, first, second);
        assert(first == 7);
        assert(second == 1);
        // A random large value
        distribute(1_000_007, first, second);
        assert(first == 7);
        assert(second == 1_000_000);
    }

    void main()
    {
        int first;
        int second;
        distribute(123, first, second);
        writeln("first: ", first, " second: ", second);
    }


Программа может быть скомпилирована и запущена на терминале с помощью следующих
команд:

.. code-block:: console

    $ dmd deneme.d -w -unittest
    $ ./deneme
    first: 7 second: 116

Хотя на самом деле функция состоит всего из двух строк, существует
всего 19 нетривиальных строк, которые поддерживают ее функциональность. Можно возразить, что столько
много лишнего кода - это слишком много для такой короткой функции. Однако ошибки никогда не бывают
намеренными. Программист всегда пишет код, который, как ожидается, будет работать правильно,
но в итоге он обычно содержит различные типы ошибок.
Когда ожидания явно выражены в модульных тестах и контрактах, функции
которые изначально корректны, имеют больше шансов остаться корректными. Я рекомендую
использовать все преимущества любой функции, улучшающей корректность программы.
И модульные тесты, и контракты - эффективные инструменты для достижения этой цели. Они помогают
сократить время, затрачиваемое на отладку, эффективно увеличивая время, затрачиваемое на собственно
написания кода.

Контракты на основе выражений
-----------------------------

Хотя блоки in и out полезны для разрешения любого D-кода, проверки предусловий и
проверки постусловий обычно не более чем простые выражения assert. В качестве
удобства в таких случаях существует более короткий синтаксис контрактов на основе выражений.
Рассмотрим следующую функцию:

.. code-block:: d

    int func(int a, int b)
    in
    {
        assert(a >= 7, "a cannot be less than 7");
        assert(b < 10);
    }
    out (result)
    {
        assert(result > 1000);
    }
    do
    {
        // ...
    }


Контракт, основанный на выражениях, позволяет обойтись без фигурных скобок, явных вызовов assert и
ключевого слова do:

.. code-block:: d

    int func(int a, int b)
    in (a >= 7, "a cannot be less than 7")
    in (b < 10)
    out (result; result > 1000)
    {
        // ...
    }

Обратите внимание, что возвращаемое значение функции названо перед точкой с запятой в контракте out
контракте. Когда нет возвращаемого значения или когда контракт out не ссылается
на возвращаемое значение, точка с запятой все равно должна присутствовать:

.. code-block:: d

    out (; /* ... */)

Отключение программирования контрактов
--------------------------------------

В отличие от модульного тестирования, функции программирования контрактов включены по умолчанию.
Ключ компилятора -release отключает программирование контрактов:

.. code-block:: console

    $ dmd deneme.d -w -release

Когда программа компилируется с ключом -release, содержимое блоков in, out,
и блоков инвариантов игнорируется.

.. index::
    enforce

in блок vs enforce
------------------

В главе об утверждениях и принудительных проверках (:ref:`глава assert и enforce<глава assert и enforce>`) мы видели, что иногда трудно решить, использовать ли проверки assert или enforce.
Аналогично, иногда трудно решить, использовать ли проверки assert внутри блоков или принудительные проверки в телах функций.
Тот факт, что контрактное программирование можно отключить, говорит о том.
что контрактное программирование предназначено для защиты от ошибок программистов. По этой поэтому решение здесь должно быть основано на тех же рекомендациях, которые мы рассматривали в
главе "assert и enforce" (:ref:`глава assert и enforce<глава assert и enforce>`):

* Если проверка защищает от ошибки кодирования, то она должна быть в
  блоке in. Например, если функция вызывается только из других частей
  программы, что, вероятно, поможет в достижении ее функциональности, тогда
  ответственность за значения параметров полностью лежит на программисте. По этой причине
  предварительные условия такой функции должны быть проверены в ее блоке in.
* Если функция не может выполнить какую-либо задачу по какой-либо другой причине, включая
  недопустимые значения параметров, тогда она должна генерировать исключение, что удобно сделать с помощью
  enforce.

Чтобы увидеть пример этого, давайте определим функцию, которая возвращает фрагмент
середины другого фрагмента. Давайте предположим, что эта функция предназначена для использования
пользователями модуля, в отличие от внутренней функции, используемой самим
модулем. Поскольку пользователи этого модуля могут вызывать эту функцию с
различными и потенциально недопустимыми значениями параметров, было бы целесообразно
проверять значения параметров при каждом вызове функции. Было бы
недостаточно проверять их только во время разработки программы, после чего
контракты могут быть отключены при сборке релизной версии.
По этой причине следующая функция проверяет свои параметры, вызывая
enforce в теле функции вместо проверки assert в блоке in:

.. code-block:: d

   import std.exception;

    inout(int)[] middle(inout(int)[] originalSlice, size_t width)
    out (result)
    {
        assert(result.length == width);
    }
    do
    {
        enforce(originalSlice.length >= width);
        immutable start = (originalSlice.length - width) / 2;
        immutable end = start + width;
        return originalSlice[start .. end];
    }

    unittest
    {
        auto slice = [1, 2, 3, 4, 5];
        assert(middle(slice, 3) == [2, 3, 4]);
        assert(middle(slice, 2) == [2, 3]);
        assert(middle(slice, 5) == slice);
    }

    void main()
    {
    }

С блоками out подобной проблемы нет. Поскольку за возвращаемое значение
каждой функции отвечает программист, постусловия
всегда должны проверяться в блоках out. Приведенная выше функция следует этому
руководству

* Другим критерием, который следует учитывать при выборе между in blocks и
  enforce, является рассмотрение возможности восстановления условия. Если это
  контрактное программирование, восстанавливаемое более высокими уровнями кода, тогда может быть более уместным
  генерировать исключение,  с помощью enforce.

Упражнения
-----------

Напишите программу, которая увеличивает общее количество очков двух футбольных команд
в зависимости от результата игры.
Первые два параметра этой функции - это голы,
забитые каждой командой. Два других параметра - это очки каждой команды перед игрой.
Эта функция должна корректировать очки команд в соответствии с забитыми
ими голами. Напоминаем, что победитель получает 3 очка, а проигравший - ни
одного. В случае ничьей обе команды получают по 1 очку каждая.
Кроме того, функция должна указывать, какая команда стала победителем: 1 если
первая команда выиграла, 2, если выиграла вторая команда, и 0, если игра закончилась
вничью.
Начните со следующей программы и заполните четыре блока функции
соответствующим образом. Не удаляйте проверки assert в main(); они демонстрируют,
как ожидается, будет работать эта функция.

.. code-block:: d

    int addPoints(int goals1, int goals2, ref int points1, ref int points2)
    in
    {
        // ...
    }
    out (result)
    {
        // ...
    }
    do
    {
        int winner;
        // ...
        return winner;
    }

    unittest
    {
        // ...
    }

    void main()
    {
        int points1 = 10;
        int points2 = 7;
        int winner;
        winner = addPoints(3, 1, points1, points2);
        assert(points1 == 13);
        assert(points2 == 7);
        assert(winner == 1);
        winner = addPoints(2, 2, points1, points2);
        assert(points1 == 14);
        assert(points2 == 8);
        assert(winner == 0);
    }


Хотя я решил вернуть значение int, было бы лучше вернуть значение enum из
этой функции:

.. code-block:: d

    enum GameResult {draw, firstWon, secondWon}
    GameResult addPoints(int goals1, int goals2,ref int points1, ref int points2)
    // ...

ответы на задачи :ref:`Контрактное программирование<решения_Контрактное_программирование>`
